package com.samuel.basics.shiro;

import com.samuel.basics.constant.CommonConstant;
import com.samuel.basics.entity.UserEntity;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.crazycake.shiro.RedisCacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.LinkedList;

/**
 * 并发登录人数控制
 *
 * @author kaiji
 * @since 2020-03-17 20:21:00
 */
public class KickOutSessionControlFilter extends AccessControlFilter {

    private static final Logger logger = LoggerFactory.getLogger(KickOutSessionControlFilter.class);

    /** 踢出后到的地址 **/
    private String kickOutUrl;
    /** 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户 **/
    private boolean kickOutAfter = false;
    /** 同一个帐号最大会话数 默认1 **/
    private int maxSession = CommonConstant.MAGIC_VALUE_ONE;
    private String kickOutAttrName = "kickOut";
    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;

    private static final String KICK_OUT_SESSION_CACHE_KEY = "kick_out_session_key";

    /**
     * 	设置Cache的key的前缀
     */
    public void setCacheManager(RedisCacheManager redisCacheManager) {
        this.cache = redisCacheManager.getCache(KICK_OUT_SESSION_CACHE_KEY);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        logger.info("请求地址：{}", httpRequest.getRequestURI());
        Subject subject = getSubject(request, response);
        if(!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果没有登录，直接进行之后的流程
            return true;
        }

        Session session = subject.getSession();
        UserEntity user = (UserEntity) subject.getPrincipal();
        String username = user.getUsername();
        Serializable sessionId = session.getId();

        logger.info("进入KickOutControl, sessionId:{}", sessionId);
        //读取缓存 没有就存入
        Deque<Serializable> deque = cache.get(username);
        if(deque == null || deque.size() <= 0) {
            deque = new LinkedList<>();
            deque.push(sessionId);
            cache.put(username, deque);
            return true;
        }

        //如果队列里没有此sessionId，且用户没有被踢出；放入队列
        if(!deque.contains(sessionId) && session.getAttribute(kickOutAttrName) == null) {
            //将sessionId存入队列
            deque.push(sessionId);
        }
        logger.info("deque.size:{}", deque.size());
        //如果队列里的sessionId数超出最大会话数，开始踢人
        if (deque.size() > maxSession) {
            Serializable kickOutSessionId;
            if(kickOutAfter) {
                //如果踢出后者
                kickOutSessionId = deque.removeFirst();
            } else {
                //否则踢出前者
                kickOutSessionId = deque.removeLast();
            }
            //踢出后再更新下缓存队列
            cache.put(username, deque);
            try {
                //获取被踢出的sessionId的session对象
                Session kickOutSession = sessionManager.getSession(new DefaultSessionKey(kickOutSessionId));
                if(kickOutSession != null) {
                    //设置会话的kickOut属性表示踢出了
                    kickOutSession.setAttribute(kickOutAttrName, true);
                }
            } catch (Exception e) {
                logger.error(e.getMessage());
            }
        }
        //如果被踢出了，直接退出，重定向到踢出后的地址
        if (session.getAttribute(kickOutAttrName) != null && (Boolean) session.getAttribute(kickOutAttrName)) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) {
                logger.warn(e.getMessage());
                e.printStackTrace();
            }
            saveRequest(request);
            //重定向
            logger.info("用户登录人数超过限制, 重定向到{}", kickOutUrl);
            String reason = URLEncoder.encode("账户已超过登录人数限制", StandardCharsets.UTF_8.toString());
            String redirectUrl = kickOutUrl  + (kickOutUrl.contains("?") ? "&" : "?") + "shiroLoginFailure=" + reason;
            WebUtils.issueRedirect(request, response, redirectUrl);
            return false;
        }
        return true;
    }


    public String getKickOutUrl() {
        return kickOutUrl;
    }

    public void setKickOutUrl(String kickOutUrl) {
        this.kickOutUrl = kickOutUrl;
    }

    public boolean isKickOutAfter() {
        return kickOutAfter;
    }

    public void setKickOutAfter(boolean kickOutAfter) {
        this.kickOutAfter = kickOutAfter;
    }

    public int getMaxSession() {
        return maxSession;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public String getKickOutAttrName() {
        return kickOutAttrName;
    }

    public void setKickOutAttrName(String kickOutAttrName) {
        this.kickOutAttrName = kickOutAttrName;
    }

    public SessionManager getSessionManager() {
        return sessionManager;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    public Cache<String, Deque<Serializable>> getCache() {
        return cache;
    }

    public void setCache(Cache<String, Deque<Serializable>> cache) {
        this.cache = cache;
    }
}